/* Dowry.js
 *
 * Copyright 2006, Tim Dwelle.
 *
 * Licensed under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in
 * compliance with the License.  You may obtain a copy of
 * the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in
 * writing, software distributed under the License is
 * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
 * CONDITIONS OF ANY KIND, either express or implied.  See
 * the License for the specific language governing
 * permissions and limitations under the License.
 *
 */

$import("$");
$import("dowry.util.CssUtils");
$import("dowry.util.EventUtils");
$import("dowry.util.MessageUtils");

/**
 * Dowry is: DWR (Direct Web Remoting) + DRY (Don't Repeat
 * Yourself).
 *
 */
$class("Dowry").$as(
{
    /**
     * Constant for the prefix to place before date values
     * when using them with the <code>sendModel()</code> /
     * <code>receiveModel()</code> methods.
     *
     */
    DATE_PREFIX : "date:",

    /**
     * A mapping of widget names to the default class of
     * <code>Widget</code> to handle it.  Anything not
     * explicitly mapped will be handled using the
     * <code>Widget</code> baseclass.
     *
     */
    DEFAULT_WIDGETS : { checkbox    : "dowry.widget.CheckboxWidget",
                        radio       : "dowry.widget.CheckboxWidget",
                        ol          : "dowry.widget.ListWidget",
                        ul          : "dowry.widget.ListWidget",
                        multiselect : "dowry.widget.MultiSelectWidget",
                        select      : "dowry.widget.SelectWidget",
                        table       : "dowry.widget.TableWidget",
                        textarea    : "dowry.widget.TextAreaWidget",
                        text        : "dowry.widget.TextInputWidget",
                        hidden      : "dowry.widget.TextInputWidget",
                        password    : "dowry.widget.TextInputWidget"
                      },

    /**
     * Constant for the string representation of false.
     *
     */
    FALSE : "false",

    /**
     * Constant for the prefix to place before handler keys
     * when those keys represent regular expression matches
     * to the specified handler.
     *
     */
    HANDLER_REGEX_PREFIX : "re:",

    /**
     * Constant for the prefix to place before configuration
     * keys to have them treated as a negation.
     *
     */
    NEGATION_PREFIX : "not",

    /**
     * Constant for the string representation of null.
     *
     */
    NULL : "null",

    /**
     * Constant for the string representation of true.
     *
     */
    TRUE : "true",

    /**
     * Constant indicating that additional validation needs
     * to be performed on the remote server.  This may be
     * thrown from the validate() method.
     *
     */
    REMOTE_VALIDATE : new Object(),

    /**
     *
     * A string constant representing the version of Dowry.
     *
     */
    VERSION : "@version@",

    /**
     * A property representing the <code>CssUtils</code>
     * instance to use for CSS stuff.
     *
     */
    css : null,

    /**
     * A property representing the mapping of type names to
     * Dowry's Datatype classes.
     *
     * <p> For instance:
     *
     * <pre>
     * Dowry.datatypes["string"] = StringType;
     * Dowry.datatypes["int"] = NumberType;
     * </pre>
     *
     * <p> etc.
     *
     */
    datatypes : new Object(),

    /**
     * A property representing the mapping of entities,
     * their properties, and the type information about
     * these properties.  This mapping is not trivial.
     *
     * <p> For instance:
     *
     * <pre>
     * Dowry.entities["Employee"] =
     * {
     *    name :
     *    {
     *       type : "string",
     *       length: 10,
     *       required : true
     *    },
     *    age :
     *    {
     *       type : "int",
     *       length: 3,
     *       required: true
     *    },
     *    salary :
     *    {
     *       type : "big_decimal",
     *       required: false,
     *       precision : 10,
     *       scale: 2
     *    }
     * );
     * </pre>
     *
     * <p> etc.
     *
     */
    entities : new Object(),

    /**
     * A property representing the <code>EventUitls</code>
     * instance to use for event management stuff.
     *
     */
    event : null,

    /**
     * A property representing the mapping of url fragments
     * to the class of the appropriate page handler.  These
     * url fragments may be string literals that must be
     * part of the page's url, or a string representing a
     * regular expression pattern (prefixed by 're:').
     *
     * <p> For instance:
     *
     * <pre>
     * Dowry.handlers["/start.html"] = StartHandler;
     * Dowry.handlers["re:.+@.+\\..+"] = EmailAddressHandler;
     * </pre>
     *
     * <p> etc.
     *
     */
    handlers : new Object(),

    /**
     * A property representing the <code>MessageUtils</code>
     * instance to use for message localization stuff.
     *
     */
    msgs : null,

    /**
     * A property representing whether Dowry should use
     * multiple iterations to build out the Dowry widgets
     * (important if Dowry widgets modify the DOM to create
     * new Dowry widgets).
     *
     * <p> This multipass processing is expensive, however,
     * and so it is turned off (false) by default.
     *
     */
    multipass : false,

    /**
     * A property representing the service for handling
     * remote processing.
     *
     */
    remote : null,

    /**
     * A property representing the mapping of widget names
     * to Dowry's <code>Widget</code> classes.
     *
     * <p> For instance:
     *
     * <pre>
     * Dowry.widgets["autoComplete"] = AutoCompleteWidget;
     * Dowry.widgets["calendar"] = CalendarWidget;
     * </pre>
     *
     * <p> etc.
     *
     */
    widgets : new Object(),

    /**
     * Private collection of IDs of elements that are
     * managed by Dowry.
     *
     */
    _elements : new Object(),

    /**
     * Private reference to the actual page handler
     * instance.
     *
     */
    _handler : null,

    /**
     * Private boolean indicating whether or not the
     * initializer function has fired yet.
     *
     */
    _initializerFired : false,

    /**
     * Creates a new instance of Dowry.  This is basically
     * a singleton class (as part of the class definition,
     * the class itself is is replaced by a single instance)
     * so all the constructor does is create some default
     * instances of our utility classes.
     *
     */
    Dowry : function()
    {
        this.css = new dowry.util.CssUtils();
        this.event = new dowry.util.EventUtils();
        this.msgs = new dowry.util.MessageUtils();
    },

    /**
     * Takes an array of values and binds it to the
     * specified element.  For Dowry elements with a
     * <code>ListWidget</code>, this is simply a convenient
     * alternative to calling
     * <code>ele.dowryWidget.addValues(vals)</code>.  If the
     * element cannot be treated as a list, this call will
     * be ignored.
     *
     *
     * @param ele         the element to add the value to
     *
     * @param vals        the array of values to add
     *
     * @param defaultVal  (optional) the default value to
     *                    be set on the list, once it is
     *                    populated
     *
     */
    addValues: function(ele, vals, defaultVal)
    {
        var widget = Dowry.getWidget(ele);
        if (widget != null &&
            widget.addValues != null)
        {
            widget.addValues(vals);

            // set the default value, if specified...
            // note that it may be specified as an
            // explicit null, so we need to check
            // the arguments to see if a default
            // value was provided
            if (arguments.length > 2)
            {
                widget.setValue(defaultVal);
            }
        }
    },

    /**
     * Populates the specified object, using the provided
     * map of configuration keys-value pairs.  The keys
     * should match the name of a field on the object.  All
     * fields may be configured, that are:
     *
     * <ol>
     * <li> not prefaced with an underscore,
     * <li> not in all capital letters,
     * <li> are a string, boolean, or number type.
     * </ol>
     *
     * <p> This method will attempt to handle basic type
     * conversion of the value to the type of the field
     * (based on the type of the field's original value).
     *
     *
     * @param obj  the object to be configured
     *
     * @param cfg  the configuration map
     *
     */
    configure : function(obj, cfg)
    {
        if (obj != null && cfg != null)
        {
            for (var key in cfg)
            {
                var val = cfg[key];
                var valType = typeof val;

                // skip keys that are prefaced with
                // underscores (these are private)
                if (key.charAt(0) == "_")
                {
                    continue;
                }

                // skip keys that are in all capital
                // letters (these are constants)
                if (key == key.toUpperCase())
                {
                    continue;
                }

                // before we start futzing with the
                // key, let's go after some low-hanging
                // fruit on value conversion
                if (valType == "string")
                {
                    // what are the odds they want to
                    // specify the sting literal null?
                    if (val == Dowry.NULL)
                    {
                        val = null;
                    }
                    else if (val == Dowry.TRUE)
                    {
                        val = true;
                    }
                    else if (val == Dowry.FALSE)
                    {
                        val = false;
                    }
                }

                // deal with our negation prefixes
                if (valType == "boolean" &&
                    key.indexOf(Dowry.NEGATION_PREFIX) == 0 &&
                    key.length > Dowry.NEGATION_PREFIX.length)
                {
                    val = !val;

                    key = key.charAt(
                              Dowry.NEGATION_PREFIX.length).toLowerCase() +
                          key.substring(
                               Dowry.NEGATION_PREFIX.length+1, key.length);
                }

                // now that we know the key, let's see
                // if it exists on the object...
                var objVal = obj[key];
                var objValType = typeof objVal;

                // can only set a value if the
                // object's value is defined
                if (objValType == "undefined")
                {
                    continue;
                }

                // anything can be null
                else if (val == null)
                {
                }

                // no way to know the type to
                // convert to... we'll need to
                // guess
                else if (objVal == null)
                {
                    // non-strings must already be
                    // converted... and since we already
                    // handled boolean conversion, we
                    // just need to check if this a
                    // number or really a string
                    if (valType == "string")
                    {
                        // if it is a valid number,
                        // treat it as such
                        var num = Number(val);
                        if (!isNaN(num))
                        {
                            val = num;
                        }
                    }
                }

                // string
                else if (objValType == "string")
                {
                    if (valType != "string")
                    {
                        val = val.toString();
                    }
                }

                // number
                else if (objValType == "number")
                {
                    if (valType != "number")
                    {
                        val = Number(val);

                        // can't convert... skip it
                        if (isNaN(val))
                        {
                            continue;
                        }
                    }
                }

                // boolean... we've already
                // handled conversion on this
                // above, so just make sure the
                // types are in argreement
                else if (objValType == "boolean")
                {
                    if (valType != "boolean")
                    {
                        continue;
                    }
                }

                // not a supported type... we
                // need to skip this one
                else
                {
                    continue;
                }

                obj[key] = val;
            }
        }
    },

    /**
     * Gets all the elements that are managed by Dowry.
     *
     *
     * @return  an array of matching elements
     *
     */
    getElements : function()
    {
        var eles = new Array();

        for (var key in Dowry._elements)
        {
            key = $(key);
            if (key != null)
            {
                eles[eles.length] = key;
            }
        }

        return eles;
    },

    /**
     * Gets the page handler instance managed by Dowry.
     *
     *
     * @return  the page handler instance
     *
     */
    getHandler : function()
    {
        return Dowry._handler;
    },

    /**
     * Gets the value on the specified element.  For Dowry
     * managed elements, this is simply a convenient
     * alternative to calling
     * <code>ele.dowryWidget.getValue()</code>.
     *
     *
     * @param  ele  the element to get to the value from
     *
     * @return      the value
     *
     */
    getValue : function(ele)
    {
        var val = null;
        var widget = Dowry.getWidget(ele);
        if (widget != null)
        {
            val = widget.getValue();
        }
        return val;
    },

    /**
     * Gets the <code>Widget</code> matching the specified
     * element.  If the element exists, but does not
     * currently have a widget associated with it, a
     * "default" widget will be created.
     *
     *
     * @param ele   the ID of the element associated with
     *              this widget
     *
     * @return      the widget
     *
     */
    getWidget : function(ele)
    {
        var widget = null;

        // only need a widget if this is a
        // valid element
        ele = $(ele);
        if (ele != null)
        {
            // return the widget associated
            // with the element
            if (ele.dowryWidget != null)
            {
                widget = ele.dowryWidget;
            }

            // if we can't find a widget, then
            // build a default one and start it
            // immediately...
            else if (ele.id != null)
            {
                var widgetClass = Dowry._getDefaultWidget(ele);
                widget = Dowry._createWidget(widgetClass, ele);
                if (ele.dowryWidget != null)
                {
                    ele.dowryWidget.start();
                }
            }
        }

        return widget;
    },

    /**
     * Initializes the page by registering HTML elements
     * according to the types and entities used by Dowry.
     * There are two ways that an HTML element can be
     * associated to Dowry:
     *
     * <p> 1. by explicit type or widget
     *
     * <pre>
     * &lt;input id="foo"
     *        type="text"
     *        class="int required"/&gt;
     * </pre>
     *
     * <p> In the above case, the CSS class declaration
     * matches a type managed by Dowry (ie. "int"), so it
     * initializes this input field according to the
     * explicitly declared parameters in the class
     * declaration.
     *
     * <p> 2. by entity mapping
     *
     * <p> More commonly, you will map based on the entity
     * mapping:
     *
     * <pre>
     * &lt;input id="age"
     *        type="text"
     *        class="Employee"/&gt;
     * </pre>
     *
     * <p> In this case, the CSS class declaration matches
     * an entity managed by Dowry (ie. "Employee").  Dowry
     * will determine the property of the entity based on
     * the element's id (or, failing to find a match, the
     * element's name).  So, again, here the entity would be
     * "Employee" and the property would be "age".  It then
     * would use the datatype configuration associated with
     * this property to initialize the input field.
     *
     * <p> NOTE:
     *
     * <p> Because <code>initialize()</code> depends so
     * heavily on the datatypes, entities, and widgets that
     * Dowry is mapped to, be sure these properties are set
     * properly prior to calling <code>initialize()</code>.
     *
     *
     * @param ele           (optional) the element to
     *                      initialize. If not specified,
     *                      the entire page is initialized
     *
     * @Param initHandler   (optional) boolean indicating
     *                      whether or not to init the
     *                      handler.  By default, this is
     *                      is true if ele is null and
     *                      false if ele is defined
     *
     */
    initialize : function(ele, initHandler)
    {
        if (initHandler == null)
        {
            initHandler = (ele == null);
        }

        Dowry._buildExplicitWidgets(ele);
        Dowry._buildImplicitWidgets(ele);
        Dowry._startWidgets();

        if (initHandler)
        {
            Dowry._buildHandler();
        }
    },

    /**
     * Populates the HTML elements based on the object model
     * representation of the specified entity.
     *
     * <p> For instance:
     *
     * <pre>
     * populateElements("Employee", employee);
     * populateElements("Address", employee.address);
     * </pre>
     *
     * <p> Notice that the entire object graph is not
     * traversed, so only the object model for the
     * "Employee" entity is used to populate on the first
     * call.  Since, in this example, the "Employee" entity
     * has a nested entity "Address" that we are also
     * interested in, we make a second call to
     * <code>populateElements()</code> to populate those
     * UI components.
     *
     *
     * @param entity  the string entity name indicating the
     *                type of entity this object represents
     *
     * @param obj     the object to bind to the elements
     *
     */
    populateElements : function(entity, obj)
    {
        Dowry.populateElementsAt(null, entity, obj);
    },

    /**
     * Populates the HTML elements based on the object model
     * representation of the specified entity.  This differs
     * from <code>populateElements()</code> only that you
     * can specify a parent element-- indicating that all
     * elements to be populated must live within this
     * specified parent.
     *
     *
     * @param parentEle    the parent element the elements
     *                     must exist under (if null, the
     *                     elements may exist anywhere on
     *                     the page)
     *
     * @param entity       the string entity name indicating
     *                     the type of entity this object
     *                     represents
     *
     * @param obj          the object to bind to the
     *                     elements
     *
     */
    populateElementsAt : function(parentEle, entity, obj)
    {
        parentEle = $(parentEle);

        if (obj != null &&
            typeof entity == "string")
        {
            // loop through all dowry-managed
            // elements
            var eles = Dowry.getElements();
            for (var i=0; i < eles.length; i++)
            {
                var ele = eles[i];

                // check if we have a matching
                // element
                if (ele.dowryWidget != null &&
                    ele.dowryDatatype != null &&
                    ele.dowryDatatype.entity == entity &&
                    ele.dowryDatatype.property != null &&
                    Dowry._isAncestor(ele, parentEle))
                {
                    // populate the widget for
                    // this object property
                    var val = obj[ele.dowryDatatype.property];
                    ele.dowryWidget.setValue(val);
                }
            }
        }
    },

    /**
     * Populates the object model representation of the
     * specified entity.
     *
     * <p> For instance:
     *
     * <pre>
     * var employee = populateObject("Employee");
     *
     * employee.address = populateObject("Address",
     *                                   employee.address);
     * </pre>
     *
     * <p> Notice that the entire object graph is not
     * traversed, so only the object model for the
     * "Employee" entity is created with the first call.
     * Since, in this example, the "Employee" entity has a
     * nested entity "Address" that we are also interested
     * in, we make a second call to
     * <code>populateObject()</code> to build out the entire
     * object graph.
     *
     *
     * @param entity  the string entity name indicating the
     *                type of entity this object represents
     *
     * @param obj     (optional) the object to be
     *                populated.  If an object is not
     *                provided, a new object will be created
     *                and used
     *
     * @return        the populated object
     *
     */
    populateObject : function(entity, obj)
    {
        return Dowry.populateObjectAt(null, entity, obj);
    },

    /**
     * Populates the object model representation of the
     * specified entity.  This differs from
     * <code>populateObject()</code> only that you can
     * specify a parent element-- indicating that all
     * elements to populate the object from must live within
     * this specified parent.
     *
     *
     * @param parentEle  the parent element the elements
     *                   must exist under (if null, the
     *                   elements may exist anywhere on
     *                   the page)
     *
     * @param entity     the string entity name indicating
     *                   the type of entity this object
     *                   represents
     *
     * @param obj        (optional) the object to be
     *                   populated.  If an object is not
     *                   provided, a new object will be
     *                   created and used
     *
     * @return           the populated object
     *
     */
    populateObjectAt : function(parentEle, entity, obj)
    {
        parentEle = $(parentEle);

        // if no object is provided, assume
        // they want us to create one
        if (obj == null)
        {
            obj = new Object();
        }

        if (typeof entity == "string")
        {
            // loop through all dowry-managed
            // elements
            var eles = Dowry.getElements();
            for (var i=0; i < eles.length; i++)
            {
                var ele = eles[i];

                // check if we have a matching
                // element
                if (ele.dowryWidget != null &&
                    ele.dowryDatatype != null &&
                    ele.dowryDatatype.entity == entity &&
                    ele.dowryDatatype.property != null &&
                    Dowry._isAncestor(ele, parentEle))
                {
                    // populate the object for
                    // this element
                    var val = ele.dowryWidget.getValue();
                    obj[ele.dowryDatatype.property] = val;
                }
            }
        }

        return obj;

    },

    /**
     * Receives the object model out of the current
     * document's URL query string.  Typically, this is
     * used with query strings that are produced via
     * <code>sendObject()</code>, but in practice, it can
     * be used with any properly constructed query string.
     *
     * <p> For instance:
     *
     * <pre>
     * somePage.html?name=Bob&colors[0]=red&colors[1]=white&
     * colors[2]=blue&address.city=Richmond&address.state=VA
     * </pre>
     *
     * <p> returns an object like the following:
     *
     * <pre>
     * var model =
     * {
     *    name: "Bob",
     *    colors : [ "red, "white", "blue"],
     *    address:
     *    {
     *       city: "Richmond",
     *       state: "VA"
     *    }
     * };
     * </pre>
     *
     * <p> when you call:
     *
     * <pre>
     * var model = Dowry.receiveObject();
     * </pre>
     *
     *
     * @return  the object model representation of the
     *          current document's URL query string
     *
     */
    receiveObject : function()
    {
        var model = null;

        // get the raw query string
        var query = document.location.search;
        if (query != null)
        {
            // create our base object
            // (this is the one we want to
            // populate from the query string)
            model = new Object();

            // parse the query string into
            // key-value pairs...
            var pairs = query.match(/^\??(.*)$/)[1].split("&");
            for (var i=0; i < pairs.length; i++)
            {
                var pair = pairs[i].split("=");
                var key = pair[0];
                var val = pair[1];

                // make sure we have a valid
                // key-value pair...
                if (key == null ||
                    val == null)
                {
                    continue;
                }

                // these query string parameters may
                // be encoded... we will decode them,
                // but unfortunately spaces often get
                // sent as + signs, which the decode
                // function does not seem to handle...
                // so if we see a + in the raw value,
                // we will convert it to %20 (the
                // escape sequence for a space), which
                // it should decode properly
                key = decodeURIComponent(key);
                val = val.replace("\+", "%20");
                val = decodeURIComponent(val);

                //create an array of arguments
                // suitable for a call to $set()
                var args = new Array();
                args[0] = model;

                // ok, do the easy part... split
                // up the nodes based on the dot
                // separator
                key = key.split(".");
                for (var j=0; j < key.length; j++)
                {
                    var node = key[j];

                    // each node may also contain
                    // an array index on the end
                    var leftBracket = node.indexOf("[");
                    var rightBracket = node.indexOf("]");

                    if (leftBracket > -1 &&
                        rightBracket == node.length-1 &&
                        leftBracket+1 < rightBracket)
                    {
                        var propName = node.substring(0, leftBracket);

                        var propIndex = node.substring(leftBracket+1,
                                                       rightBracket);

                        // an empty index (this is supported
                        // in $set()...)
                        if (propIndex == "")
                        {
                            propIndex = [];
                        }

                        // otherwise, assume a numeric
                        // index
                        else
                        {
                            propIndex = Number(propIndex);
                        }

                        // add both nodes
                        args[args.length] = propName;
                        args[args.length] = propIndex;
                    }

                    // no array notation... simple node
                    else
                    {
                        args[args.length] = node;
                    }
                }

                // try to guess the datatype of the
                // value

                // if it parses to a number, then
                // treat as number
                var num = Number(val);
                if (!isNaN(num))
                {
                    val = num;
                }

                // string literal for true and false
                // are treated as boolean
                else if (val == Dowry.TRUE)
                {
                    val = true;
                }
                else if (val == Dowry.FALSE)
                {
                    val = false;
                }

                // if it uses the format 'date(12345)'
                // then assume it is a date, and the
                // number between the parentheses is
                // the timestamp
                else if (val.indexOf(Dowry.DATE_PREFIX + "(") == 0 &&
                         val.indexOf(")") == val.length-1)
                {
                    num = val.substring(Dowry.DATE_PREFIX.length+1,
                                        val.length-1);

                    num = Number(num);
                    if (!isNaN(num))
                    {
                        val = new Date(num);
                    }
                }

                // add the value as the final node
                args[args.length] = val;

                // now set it!
                Dowry.$set.apply(null, args);
            }
        }

        return model;
    },

    /**
     * Removes all data from the specified element.  For
     * Dowry elements with a ListWidget, this is simply a
     * convenient alternative to calling
     * <code>ele.dowryWidget.removeAllValues()</code>.  If
     * the element cannot be treated as a list, this call
     * will be ignored.
     *
     *
     * @param ele   the element to add the value to
     *
     */
    removeAllValues : function(ele)
    {
        var widget = Dowry.getWidget(ele);

        if (widget != null &&
            widget.removeAllValues != null)
        {
            widget.removeAllValues();
        }
    },

    /**
     * Submits a GET request to the specified URL.  The
     * query string will include a serialized representation
     * of the specified model.
     *
     * <p> For instance:
     *
     * <pre>
     * var model =
     * {
     *    name: "Bob",
     *    colors : [ "red, "white", "blue"],
     *    address:
     *    {
     *       city: "Richmond",
     *       state: "VA"
     *    }
     * };
     *
     * Dowry.sendObject("somePage.html", model);
     * </pre>
     *
     * <p> submits a request like:
     *
     * <pre>
     * somePage.html?name=Bob&colors[0]=red&colors[1]=white&
     * colors[2]=blue&address.city=Richmond&address.state=VA
     * </pre>
     *
     *
     * @param url    the url to send to
     *
     * @param obj    the object model representation to send
     *               to the server via the query string
     *
     */
    sendObject : function(url, obj)
    {
        if (typeof url == "string")
        {
            var hiddenForm = document.createElement("form");
            hiddenForm.setAttribute("method", "GET");
            hiddenForm.setAttribute("action", url);
            document.body.appendChild(hiddenForm);

            if (obj != null)
            {
                for (var key in obj)
                {
                    Dowry._sendObjectField(hiddenForm, key, obj[key]);
                }
            }

            hiddenForm.submit();
        }
    },

    /**
     * Sets the value on the specified element.  For Dowry
     * managed elements, this is simply a convenient
     * alternative to calling
     * <code>ele.dowryWidget.setValue(val)</code>.
     *
     *
     * @param  ele  the element to get to the value from
     *
     * @param val   the value to set
     *
     */
    setValue : function(ele, val)
    {
        var widget = Dowry.getWidget(ele);
        if (widget != null)
        {
            widget.setValue(val);
        }
    },

    /**
     * Validates all Dowry-initialized elements. This works
     * nearly identially to multiple calls to
     * <code>datatype.validate()</code>, except that it
     * takes an optional callback function with a boolean
     * indicating whether or not the entire structure is
     * valid.
     *
     * <p> If no callback is passed, then the function will
     * return a boolean true if no validation errors were
     * found, false if validation errors were found, and
     * throw <code>Datatype.prototype.REMOTE_VALIDATE</code>
     * if the results of validation were indeterminate,
     * because remote validation has not finished executing.
     *
     *
     * @param callback  (optional) the callback function
     *                  should take a boolean argument
     *                  indicating whether or not the
     *                  structure is valid
     *
     * @return          (optional) if no callback function
     *                  is provided, returns true if
     *                  validation finds no errors; false if
     *                  validation finds errors; and throws
     *                  REMOTE_VALIDATE if the results of
     *                  validation were indeterminate
     *                  because remote validation is pending
     *
     */
    validate : function(callback)
    {
        var ok = true;

        var remote = new Array();

        var eles = Dowry.getElements();
        for (var i=0; i < eles.length; i++)
        {
            var ele = eles[i];

            if (ele.dowryWidget != null &&
                ele.dowryDatatype != null)
            {
                try
                {
                    // if the field already has a message,
                    // then can we trust it?
                    var msg = ele.dowryWidget.getErrorMessage();
                    if (msg == null)
                    {
                        var val = ele.dowryWidget.getValue();
                        msg = ele.dowryDatatype.validate(val);
                        ele.dowryWidget.setErrorMessage(msg);
                    }

                    if (msg != null)
                    {
                        // focus on the first field with an
                        // error
                        if (ok)
                        {
                            try
                            {
                                ele.focus();
                            }
                            catch(e)
                            {
                            }
                        }
                        ok = false;
                    }
                }
                catch(e)
                {
                    // if an element needs to be remotely
                    // validated, just capture that for now
                    if (e == Dowry.REMOTE_VALIDATE)
                    {
                        var cfg = ele.dowryDatatype.toConfiguration();
                        cfg.id = ele.id;
                        cfg.value = val;

                        remote[remote.length] = cfg;
                    }
                    else
                    {
                        throw e;
                    }
                }
            }
        }

        // no remote validation required, simply fire the
        // callback function or return the return code
        if (remote.length == 0)
        {
            if (typeof callback == "function")
            {
                callback.call(null, ok);
            }
            else
            {
                return ok;
            }
        }

        // fire the rest of the remote validation
        else
        {
            // handle remote validation
            if (Dowry.remote != null &&
                Dowry.remote.validateMany != null)
            {
                var func =
                    Dowry._setErrorMessages.callback($VALUE, callback, ok);

                Dowry.remote.validateMany(remote, func);
            }

            // we have already kicked off remote
            // validation... but we need to throw
            // the exception to let them know that
            // we can't return a reply... since we're
            // still waiting for validation to finish
            if (callback == null)
            {
                throw Dowry.REMOTE_EXCEPTION;
            }
        }
    },

    /**
     * Initialize widgets that are explicitly defined as
     * such, based on their CSS class name.  This will
     * process recursively, so that if the process of
     * initializing an explicit widget in turn, creates
     * a new widget... those explicitly defined widgets
     * will be initialized as well.
     *
     *
     * @param root  the root element to search under
     *
     */
    _buildExplicitWidgets : function(root)
    {
        var lastCount = -1;
        var count = 0;

        // determine all of our widget keys
        var widgetKeys = new Array();
        for (var key in Dowry.widgets)
        {
            widgetKeys[widgetKeys.length] = key;
        }

        // process recursively until we
        // are done building widgets...
        // (no new widgts added to the count...
        while (count != lastCount)
        {
            lastCount = count;
            count = 0;

            var eles = Dowry.css.getElementsByClassName(widgetKeys, root);
            for (var i=0; i < eles.length; i++)
            {
                var ele = eles[i];

                // already created... just include
                // in the count
                if (ele.dowryWidget != null)
                {
                    count++;
                }

                // otherwise, build the widget...
                else if (ele.id != null)
                {
                    var widgetClass = Dowry.widgets[
                        Dowry._getClass(ele, Dowry.widgets)];

                    Dowry._createWidget(widgetClass, ele);
                    count++;
                }
            }

            // this will prevent multiple passes...
            if (!Dowry.multipass)
            {
                lastCount = count;
            }
        }
    },

    /**
     * Initializes the page handler component for this
     * page's URL.
     *
     */
    _buildHandler : function()
    {
        if (Dowry._handler == null)
        {
            var handlerClass = null;

            // look for a match based on the url of the page
            var url = document.location.pathname;
            if (url != null)
            {
                for (var key in Dowry.handlers)
                {
                    // we know its a regular expression if it
                    // begins with the regex prefix
                    if (key.indexOf(Dowry.HANDLER_REGEX_PREFIX) == 0)
                    {
                        var re = new RegExp(
                            key.substring(Dowry.HANDLER_REGEX_PREFIX.length));

                        if (re.test(url))
                        {
                            handlerClass = Dowry.handlers[key];
                            break;
                        }
                    }

                    // otherwise, its just a string literal
                    // that needs to be matched
                    else
                    {
                        if (url.indexOf(key) > -1)
                        {
                            handlerClass = Dowry.handlers[key];
                            break;
                        }
                    }
                }
            }

            // create page handler
            Dowry._handler = Dowry._createHandler(handlerClass);
        }
    },

    /**
     * Initialize widgets that are implicitly defined--
     * either because they contain a CSS class name
     * specifying a specific datatype, or because they
     * contain a CSS class name mapping them to a Dowry
     * managed entity.  This method will initialize the
     * datatype, as well as "guess" the appropriate widget
     * for this element and initialize it.
     *
     * <p> This will process recursively, so that if the
     * process of initializing an implicit widget in
     * turn, creates a new widget... those implicitly
     * defined widgets will be initialized as well.  Note
     * that this will have fired *after* the
     * <code>buildExplicitWidgets()</code> process, so
     * implicit widgets should avoid altering the HTML such
     * that an explicit widget must be created.
     *
     *
     * @param root  the root element to search under
     *
     */
    _buildImplicitWidgets : function(root)
    {
        var lastCount = -1;
        var count = 0;

        if (Dowry.datatypes != null &&
            Dowry.entities != null)
        {
            // determine all of our datatype keys
            var datatypeKeys = new Array();
            for (var key in Dowry.datatypes)
            {
                datatypeKeys[datatypeKeys.length] = key;
            }

            // determine all of our entity keys
            var entityKeys = new Array();
            for (var key in Dowry.entities)
            {
                entityKeys[entityKeys.length] = key;
            }

            // process recursively until we
            // are done building widgets...
            // (ie. no new widgts added to the count)
            while (count != lastCount)
            {
                lastCount = count;
                count = 0;

                var eles = Dowry.css.getElementsByClassName(datatypeKeys, root);
                for (var i=0; i < eles.length; i++)
                {
                    var ele = eles[i];

                    // already created... just include
                    // in the count
                    if (ele.dowryWidget != null &&
                        ele.dowryDatatype != null)
                    {
                        count++;
                    }

                    // otherwise, build out the datatype
                    // or widget as needed
                    else if (ele.id != null)
                    {
                        // build datatype...
                        if (ele.dowryDatatype == null)
                        {
                            var key = Dowry._getClass(ele, Dowry.datatypes);

                            var cfg = { type: key };

                            var datatypeClass = Dowry.datatypes[key];

                            Dowry._createDatatype(datatypeClass, ele, cfg);
                        }

                        // build widget...
                        if (ele.dowryWidget == null)
                        {
                            var widgetClass = Dowry._getDefaultWidget(ele);
                            Dowry._createWidget(widgetClass, ele);
                            count++;
                        }
                    }
                }

                eles = Dowry.css.getElementsByClassName(entityKeys, root);
                for (var i=0; i < eles.length; i++)
                {
                    var ele = eles[i];
                    var entity = Dowry._getClass(ele, Dowry.entities);
                    var properties = Dowry.entities[entity];

                    // determine which property matches
                    // our element (the element's id or
                    // name)
                    var property = null;

                    if (properties != null)
                    {
                        // element id
                        if (properties[ele.id] != null)
                        {
                            property = ele.id;
                        }

                        // element name
                        if (property == null)
                        {
                            var name = ele.name;

                            // name is not a "real" attribute of
                            // anything but form fields... so if
                            // we can't find a name for the element,
                            // see if there if an attribute specified
                            // via the DOM... of course, using this
                            // attribute may mean the page is not
                            // strictly valid html anymore...
                            if (name == null)
                            {
                                name = ele.getAttribute("name");
                            }

                            if (properties[name] != null)
                            {
                                property = name;
                            }
                        }
                    }

                    if (property != null &&
                        ele.id != null)
                    {
                        var propertyCfg = properties[property];
                        var mappingCfg = { entity: entity,
                                           property: property };

                        // create a datatype, if one does not exist...
                        if (ele.dowryDatatype == null)
                        {
                            var datatypeClass =
                                Dowry.datatypes[propertyCfg.type];

                            Dowry._createDatatype(datatypeClass,
                                                  ele,
                                                  propertyCfg);
                        }

                        // got a datatype... just augment configuration
                        // with entity mapping stuff...
                        else
                        {
                            mappingCfg.type = ele.dowryDatatype.type;

                            Dowry._createDatatype(ele.dowryDatatype,
                                                  ele,
                                                  propertyCfg);
                        }

                        // whether or not datatype exists, configure it
                        // to include the mapping configuration
                        Dowry.configure(ele.dowryDatatype, mappingCfg);

                        // create a widget, if one does not exist...
                        if (ele.dowryWidget == null)
                        {
                            var widgetClass = Dowry._getDefaultWidget(ele);
                            Dowry._createWidget(widgetClass, ele);
                            count++;
                        }

                        // widget already exists, just add it to the count
                        else
                        {
                            count++;
                        }
                    }
                }

                // this will prevent multiple passes...
                if (!Dowry.multipass)
                {
                    lastCount = count;
                }
            }
        }
    },

    /**
     * Create a datatype instance for the specified class,
     * associated to the provided element.  If additional
     * configuration is provided (representing an entity
     * mapping) it will be applied.
     *
     *
     * @param dtClass   the class for the datatype (or
     *                  optionally, the existing datatype
     *                  instance associated with the
     *                  provided element)
     *
     * @param ele       the element associated to this
     *                  datatype
     *
     * @param cfg       the entity mapping configuration
     *
     * @return          the datatype instance
     *
     */
    _createDatatype : function(dtClass, ele, cfg)
    {
        var dt = null;

        ele = $(ele);

        // existing instance to be re-configurated...
        if (ele != null &&
            ele.dowryDatatype == dtClass)
        {
            dt = dtClass;
        }

        // new instance to be created...
        else if (ele != null &&
                 ele.id != null &&
                 typeof dtClass == "string")
        {
            dtClass = $classLoader.loadClass(dtClass);

            if (dtClass != null)
            {
                dt = new dtClass;
            }
        }

        if (dt != null)
        {
            // apply entity mapping cfg
            if (cfg != null)
            {
                Dowry.configure(dt, cfg);
            }

            // apply element-specific cfg
            cfg = Dowry._toConfiguration(ele);
            Dowry.configure(dt, cfg);

            ele.dowryDatatype = dt;
        }

        return dt;
    },

    /**
     * Create a page handler instance for the specified
     * class.  The handler's constructor will be wrapped so
     * the order of DWR calls are guaranteed.
     *
     * @param handlerClass  the class for the page
     *
     * @return              the page handler instance
     *
     */
    _createHandler : function(handlerClass)
    {
        var handler = null;

        if (typeof handlerClass == "string")
        {
            handlerClass = $classLoader.loadClass(handlerClass);

            if (handlerClass != null)
            {
                handler = new handlerClass;
            }
        }

        return handler;
    },

    /**
     * Create a widget instance for the specified class,
     * associated to the provided element.
     *
     *
     * @param widgetClass  the string classname for the
     *                     widget
     *
     * @param ele          the element associated to this
     *                     widget
     *
     * @return             the widget instance
     *
     */
    _createWidget : function(widgetClass, ele)
    {
        var widget = null;

        ele = $(ele);

        if (ele != null &&
            ele.id != null &&
            typeof widgetClass == "string")
        {
            widgetClass = $classLoader.loadClass(widgetClass);
            if (widgetClass != null)
            {
                widget = new widgetClass(ele);
                if (widget != null)
                {
                    var cfg = Dowry._toConfiguration(ele);
                    Dowry.configure(widget, cfg);

                    ele.dowryWidget = widget;
                    Dowry._elements[ele.id] = null;
                }
            }
        }

        return widget;
    },

    /**
     * Determines which css classname from this map is the
     * matching css classname.
     *
     *
     * @param ele        the element
     *
     * @param map        map with the possible css classes
     *                   as keys
     *
     * @return           the first match
     *
     */
    _getClass : function(ele, map)
    {
        var rc = null;

        // as an efficiency trick, css utils sets
        // this "magic" flag on the element
        // indicating the class name that matched
        // on the getElementByClassName() call...
        // try this first...
        if (ele.dowryMatchingClassName != null)
        {
            rc = ele.dowryMatchingClassName;
        }

        // if no "magic" flag, then we'll search
        // again for it... one of the css clases
        // on this element should be a key in the
        // provided map
        else
        {
            var cssClasses = Dowry.css.getClassNames(ele);
            for (var i=0; i < cssClasses.length; i++)
            {
                var feature = cssClasses[i];
                if (map[feature] != null)
                {
                    rc = feature;
                    break;
                }
            }
        }

        return rc;
    },

    /**
     * Determines the default widget class for this
     * element.
     *
     *
     * @param ele  the element
     *
     * @return     the class of the associated widget
     *
     */
    _getDefaultWidget : function(ele)
    {
        var widgetClass = null;

        // first try to get the type based
        // on the name of the element
        if (ele != null)
        {
            var type = ele.nodeName.toLowerCase();

            // we need more info if the field
            // is an input field... we will use
            // the type instead of the element
            // name...
            if (type == "input" &&
                ele.type != null)
            {
                type = ele.type.toLowerCase();

                // checkboxes are inconsistently
                // named... so if it looks kinda
                // like checkbox, we will normalize
                // it to 'checkbox'
                if (type.indexOf("check") > -1)
                {
                    type = "checkbox";
                }
            }

            // multiselects should really be
            // handled different than single
            // value selects
            else if (type == "select" &&
                     ele.multiple)
            {
                type = "multiselect";
            }
        }

        // try to lookup the default widget for
        // this element type
        widgetClass = Dowry.DEFAULT_WIDGETS[type];

        // this is the default default
        // widget... tee hee...
        if (widgetClass == null)
        {
            widgetClass = "dowry.widget.Widget";
        }

        return widgetClass;
    },

    /**
     * Custom function to handle onload initialization,
     * using the Dean Edwards "onload problem solved"
     * solution.
     *
     * http://dean.edwards.name/weblog/2005/09/busted/
     *
     */
    _initializer : function()
    {
        if (!Dowry._initializerFired)
        {
            Dowry._initializerFired = true;
            Dowry.initialize();
        }
    },

    /**
     * Checks to see if the provided element is an ancestor.
     *
     * @param childEle   the child
     *
     * @param parentEle  the parent
     *
     * @return           a boolean indicating whether or
     *                   not they are ancestors
     *
     */
    _isAncestor : function(childEle, parentEle)
    {
        var ancestor = false;

        if (childEle != null &&
            parentEle != null)
        {
            while (childEle != null)
            {
                if (childEle == parentEle)
                {
                    ancestor = true;
                    break;
                }
                else
                {
                    childEle = childEle.parentNode;
                }
            }
        }
        else
        {
            ancestor = true;
        }

        return ancestor;
    },

    /**
     * Recursive function to build out a form containing
     * the "serializing representation" of the value.
     *
     * @param hiddenForm   the form
     *
     * @param key          the key for this value
     *
     * @param val          the value to build from
     *
     * @param prefix       the prefix to prepend to the
     *                     key-value pair
     *
     */
    _sendObjectField : function(hiddenForm, key, val, prefix)
    {
        if (val != null)
        {
            // determine full name
            if (prefix == null)
            {
                prefix = key;
            }
            else if (typeof key == "number")
            {
                prefix = prefix + "[" + key + "]";
            }
            else
            {
                prefix = prefix + "." + key;
            }

            // convert date to a string form
            if (val.getUTCDate != null)
            {
                val = Dowry.DATE_PREFIX + "("+val.valueOf()+")";
            }

            // simple types
            var valType = typeof val;
            if (valType == "string" ||
                valType == "boolean" ||
                valType == "number")
            {
                var ele = document.createElement("input");
                ele.setAttribute("type", "hidden");
                ele.setAttribute("name", prefix);
                ele.setAttribute("value", val);
                hiddenForm.appendChild(ele);
            }

            // array
            else if (val.join != null)
            {
                for (var i=0; i < val.length; i++)
                {
                    Dowry._sendObjectField(hiddenForm, i, val[i], prefix);
                }
            }

            // object
            else
            {
                for (var key in val)
                {
                    Dowry._sendObjectField(hiddenForm, key, val[key], prefix);
                }
            }
        }
    },

    /**
     * Takes a map of element IDs to validation messages,
     * and sets them all.  Will send a boolean indicating
     * whether this mapping contains any validation
     * messages to the provided callback.
     *
     *
     * @param msg       a mapping of element id -> error
     *                  messages
     *
     * @param callback  (optional) a callback that takes in
     *                  a boolean parameter to indicate
     *                  whether validation errors were found
     *                  in the mapping
     *
     * @param ok        (optional) the ok status of the
     *                  validation prior to setting these
     *                  validation messages
     *
     */
    _setErrorMessages : function(msgs, callback, ok)
    {
        // take in the current ok status... but if
        // not provided, assume it was ok to start with...
        if (ok == null)
        {
            ok = true;
        }

        if (msgs != null)
        {
            var firstMsg = true;

            for (var ele in msgs)
            {
                var msg = msgs[ele];
                var widget = Dowry.getWidget(ele);

                if (msg != null)
                {
                    // focus on the first field with
                    // an error message
                    if (ok)
                    {
                        try
                        {
                            widget.getElement().focus();
                        }
                        catch(e)
                        {
                        }
                    }

                    ok = false;
                }

                if (widget != null)
                {
                    widget.setErrorMessage(msg);
                }
            }
        }

        if (typeof callback == "function")
        {
            callback.call(null, ok);
        }
    },

    /**
     * Start all the explicit and implicit widgets that have
     * been created during <code> initialize() </code>.
     *
     */
    _startWidgets : function()
    {
        var eles = Dowry.getElements();
        for (var i=0; i < eles.length; i++)
        {
            var ele = eles[i];
            if (ele.dowryWidget != null &&
                !ele.dowryWidget._started)
            {
                var byType = Dowry._startWidgets.byType;
                if (byType == null)
                {
                    byType = {};
                    Dowry._startWidgets.byType = byType;
                }
                if (byType[ele.dowryWidget.$class] == null)
                {
                    byType[ele.dowryWidget.$class] =
                       { callCount : 0, duration : 0};
                }
                byType = byType[ele.dowryWidget.$class];
                var start = new Date();

                ele.dowryWidget.start();

                var stop = new Date();
                byType.callCount++;
                byType.duration += stop.getTime() - start.getTime();

                ele.dowryWidget._started = true;
            }
        }
    },

    /**
     * Gets the CSS class names and converts them into
     * a map of configuration key-value pairs.
     *
     *
     * @param ele   the element to get the configuration out
     *              of
     *
     * @return      a map of key-value pairs
     *
     */
    _toConfiguration : function(ele)
    {
        var cfg = null;

        // already called toConfiguration on this
        // element... use cached configuration...
        if (ele.dowryCfg != null)
        {
            cfg = ele.dowryCfg;
        }

        // determine configuration from class names...
        else
        {
            cfg = new Object();
            var classNames = Dowry.css.getClassNames(ele);
            for (var i=0; i < classNames.length; i++)
            {
                var className = classNames[i];
                var dashIndex = className.indexOf("-");

                // no dash or else dash at the
                // start or end of className
                if (dashIndex < 1 ||
                    dashIndex == className.length-1)
                {
                    cfg[className] = true;
                }

                // dash separating a key name from a value
                else
                {
                    var key = className.substring(0, dashIndex);
                    var val = className.substring(dashIndex+1, className.length);
                    cfg[key] = val;
                }
            }
            ele.dowryCfg = cfg;
        }

        return cfg;
    },

    /**
     * TODO: got to be a better place to put this!
     *
     * A null-safe way to get the specified property. Just
     * pass in the first argument as the object, and then
     * subsequent arguments should be the properties to
     * navigate.
     *
     * <p> Usage:
     *
     * <pre>
     * // null-safe equivalent to o.name
     * var a = $get(o, "name");
     *
     * // null-safe equivalent to o.address.city
     * var b = $get(o, "address", "city");
     *
     * // null-safe equivalent to o.phone[0]
     * var c = $get(o, "phone", 0);
     *
     * // alternative way to navigate an array...
     * var d = $get(o, "phone", [0]);
     * </pre>
     *
     *
     * @param args   the arguments should consist of an
     *               object (that may or may not be null,
     *               followed by 0 or more property names)
     *
     * @return       the value of the property. if a null
     *               is encountered on any node in the
     *               get, then the value is returned as null
     *
     */
    $get : function(args)
    {
        var obj = null;
        var len = arguments.length;

        // the root object is the first arg
        if (len > 0)
        {
            obj = arguments[0];
        }

        // subsequent args are properties
        if (len > 1)
        {
            for (var i=1; i < len; i++)
            {
                var property = arguments[i];

                // array notation... retrieve
                // actual property from within the
                // brackets
                if (property != null &&
                    property.join != null &&
                    property.length == 1)
                {
                    property = property[0];
                }

                // navigate to the next property
                if (obj != null &&
                    property != null)
                {
                    obj = obj[property];
                }
                else
                {
                    obj = null;
                }
            }
        }

        return obj;
    },

    /**
     * TODO: got to be a better place to put this!
     *
     * A null-safe way to set the specified property. Just
     * pass in the first argument as the object, and then
     * subsequent arguments should be the properties to
     * navigate.  The last argument should be the value to
     * be set.  If any of the nodes of the object graph
     * are null, then <code>$set()</code> will populate them
     * along the way.
     *
     * <p> Usage:
     *
     * <pre>
     * // create a "blank" object
     * var o = new Object();
     *
     * // equivalent to o.name = "Bob"
     * $set(o, "name", "Bob");
     *
     * // creates an intermediary address object and
     * // then sets that object's city field to "Richmond"
     * $set(o, "address", "city", "Richmond");
     *
     * // equivalent to o.address.state = "VA", since
     * // last time we created the address object
     * $set(o, "address", "state", "VA");
     *
     * // creates a field on our object called phone, that
     * // represents an array (assumed from the integer
     * // key) and sets the value of index 0 of the array to
     * // "804-555-1212"
     * $set(o, "phone", 0, "804-555-1212");
     *
     * // another way to specify an array index in a more
     * // "natural" syntax
     * $set(o, "phone", [1], "804-555-3434");
     *
     * // an empty indexer is interpretted as 'add this element
     * // at the end of the array'... in this case, index 2
     * $set(o, "phone", [], "804-555-5656");
     *
     * // note that the set expression can be as long as
     * // necessary... and all nodes will be built out along
     * // the way.  and of course, the value can be any type,
     * // not just strings!
     * $set(o, "foo", "bar", 0, "baz", "bubba", true);
     * </pre>
     *
     *
     * @param args   the arguments should consist of an
     *               object (that may or may not be null,
     *               followed by 1 or more property names)
     *               and a value
     *
     */
    $set : function(args)
    {
        var len = arguments.length;

        if (len > 2)
        {
            // the root object is the first arg
            var obj = Dowry.$get(arguments, 0);

            // the value to set is the last arg
            var val = arguments[len-1];

            // navigate the object graph up to the
            // point of the last property
            if (obj != null)
            {
                for (var i=1; i < len-2; i++)
                {
                    var property = arguments[i];
                    var nextObj = Dowry.$get(obj, property);

                    // if we have a null object,
                    // then we will create an
                    // object to fill in the
                    // object graph
                    if (nextObj == null)
                    {
                        var nextProperty = arguments[i+1];

                        // if we are using array brackets, we
                        // must want an array
                        if (nextProperty != null &&
                            nextProperty.join != null &&
                            nextProperty.length < 2)
                        {
                            nextObj = new Array();
                        }

                        // if the next property is a number,
                        // then we can also assume that this
                        // is an array index
                        else if (typeof nextProperty == "number")
                        {
                            nextObj = new Array();
                        }

                        // in all other cases, create an object
                        else
                        {
                            nextObj = new Object();
                        }

                        // while on first blush this looks
                        // like it could cause a dangerous
                        // infinite recursion problem...
                        // a single property $set() never
                        // enters this loop, so the call
                        // below is safe
                        Dowry.$set(obj, property, nextObj);
                    }

                    // traverse down the object graph
                    obj = nextObj;
                }
            }

            // set the final property
            var property = arguments[len-2];

            if (obj != null && property != null)
            {
                // array notation... retrieve
                // actual property from within the
                // brackets
                if (property.join != null &&
                    property.length == 1)
                {
                    property = property[0];
                }

                // an empty array means
                // "add to the end"
                if (property.join != null &&
                    property.length == 0)
                {
                    property = obj.length;
                }

                // actually set the value
                if (typeof property == "string" ||
                    typeof property == "number")
                {
                    obj[property] = val;
                }
            }
        }
    }
});

Dowry = new Dowry();